from __future__ import division

from concepts import *

import math


class StorageError(Exception):
    pass


class Storage(Object):
    accepts = ListAttribute()
    unit    = Attribute(initial = 1)
    stored  = DictAttribute(initiable = False)
    
    def setup(self):
        seen = set()
        
        for good in self.accepts:
            duplicates = seen & good.type.all_subtypes
            
            if duplicates:
                raise StorageError('Storage has duplicate entries in signature: ' +
                                   (', '.join(type.__name__ for type in duplicates)))
            
            seen |= good.type.all_subtypes
    
    @property
    def storage(self):
        return self
    
    @property
    def capacity(self):
        return sum(good.quantity for good in self.accepts)
    
    @property
    def accepted_types(self):
        types = set()
        
        for good in self.accepts:
            types |= good.type.all_concrete_subtypes
        
        return types
    
    @memoized
    def signature_type_for(self, type):
        signature = set(good.type for good in self.accepts)
        
        for supertype in type.all_supertypes:
            if supertype in signature:
                return supertype
        
        return None
    
    @memoized
    def competing_types_for(self, type):
        supertype = self.signature_type_for(type)
        
        if supertype is None:
            return None
        else:
            return supertype.all_concrete_subtypes
    
    @memoized
    def maximum_storage_for(self, type):
        supertype = self.signature_type_for(type)
        
        if supertype is None:
            return 0
        else:
            for good in self.accepts:
                if good.type is supertype:
                    return good.quantity
    
    @property
    def stored_types(self):
        return set(self._stored.keys())
    
    def can_store(self, good):
        return good is not None and good.quantity > 0 and self.can_store_type(good.type)
    
    def can_store_all(self, good):
        return self.can_store(good) and self.storage_for(good.type) >= good.quantity
    
    def can_store_type(self, type):
        return type in self.accepted_types
    
    def capacity_for(self, type):
        found = [good for good in self.accepts if good.type in type.all_supertypes]
        
        return sum(good.quantity for good in found)
    
    @property
    def is_empty(self):
        return len(self.stored_types) == 0
    
    @property
    def is_full(self):
        return self.free_storage == 0
    
    @property
    def used_storage(self):
        return sum(self._stored.values())
    
    def used_storage_for(self, type):
        return sum(self._stored.get(subtype, 0) for subtype in type.all_concrete_subtypes)
    
    @property
    def reserved_storage(self):
        return sum(self.unit - (value % self.unit) for value in self._stored.values()
                   if value % self.unit > 0)
    
    def reserved_storage_for(self, type):
        return sum(self.unit - (value % self.unit) for key, value in self._stored.items()
                   if issubclass(key, type) and value % self.unit > 0)
    
    @property
    def free_storage(self):
        return self.capacity - self.used_storage
    
    @property
    def unreserved_storage(self):
        return self.free_storage - self.reserved_storage
    
    def competing_storage_for(self, type):
        return sum(self.used_storage_for(subtype) + self.reserved_storage_for(subtype)
                   for subtype in self.competing_types_for(type))
    
    def free_storage_for(self, type):
        return self.maximum_storage_for(type) - self.competing_storage_for(type) + self.reserved_storage_for(type)
    
    def storage_for(self, type):
        return self.used_storage_for(type) + self.reserved_storage_for(type)
    
    def number_of(self, type):
        return self.used_storage_for(type)
    
    def has(self, type):
        return (self.number_of(type) > 0)
    
    def all(self, type):
        return type(self.number_of(type))
    
    @property
    def all_goods(self):
        return [self.all(type) for type in self.stored_types]
    
    def add_goods(self, goods):
        if goods is None or goods.quantity < 0 or not self.can_store_type(goods.type):
            return goods            
        
        num = min(self.free_storage_for(goods.type), goods.quantity)
        
        if num > 0:
            if self.has(goods.type):
                self._stored[goods.type] += num
            else:
                self._stored[goods.type]  = num
        
        if num == goods.quantity:
            return None
        else:
            return goods - num
    
    def remove_goods(self, goods):
        if goods is None or goods.quantity < 0:
            return []
        
        taken = []
        left  = goods.quantity
        
        for type in goods.type.all_concrete_subtypes:
            if not self.has(type) or left == 0:
                continue
            
            if left < self._stored[type]:
                taken.append(type(left))
                self._stored[type] -= left
                left = 0
            else:
                taken.append(type(self._stored[type]))
                left -= self._stored[type]
                del self._stored[type]
        
        return taken
    
    def remove_all_goods(self):
        self._stored.clear()
    
    def transfer_from(self, target, goods):
        if goods is None or goods.quantity <= 0:
            return
        
        goods = goods.max(self.free_storage_for(goods.type))
        
        for removed in target.storage.remove_goods(goods):
            self.add_goods(removed)
    
    def transfer_to(self, target, goods):
        target.storage.transfer_from(self, goods)
    
    def load_from(self, target):
        target.storage.unload_into(self)
    
    def unload_into(self, target):
        for goods in self.all_goods:
            self.transfer_to(target.storage, goods)
    
    def __str__(self):
        return '{%s|%d[%d]}' % (', '.join(str(good) for good in self.all_goods), self.capacity, self.unit)
